/*
 * Copyright (C) 2018 Logos Technology Ltd.
 */

package com.logos.tool.file;

import android.content.Context;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.UUID;

/**
 * Based on some of the responses from this question
 * http://stackoverflow.com/questions/5694933/find-an-external-sd-card-location/15612964#15612964
 * Returns the first storage mount point with the most space that is writable for use as the default
 * location for the osmdroid cache. If an external mount point is not available, application private
 * storage will be used
 * Created by alex on 10/19/16.
 */

public class StorageUtils {
    private static final String SD_CARD = "sdCard";
    private static final String EXTERNAL_SD_CARD = "externalSdCard";

    public static class StorageInfo {

        public final String path;
        public final boolean internal;
        public boolean readonly;
        public final int mDisplayNumber;
        public long freeSpace = 0;
        public String mDisplayName = "";

        public StorageInfo(String path, boolean internal, boolean readonly, int displayNumber) {
            this.path = path;
            this.internal = internal;

            this.mDisplayNumber = displayNumber;
            if (Build.VERSION.SDK_INT >= 9) {
                this.freeSpace = new File(path).getFreeSpace();
            }
            if (!readonly) {
                //confirm it's writable
                File file = new File(path + File.separator + UUID.randomUUID().toString());
                try {
                    file.createNewFile();
                    file.delete();
                    this.readonly = false;
                } catch (Throwable e) {
                    this.readonly = true;
                }
            } else {
                this.readonly = readonly;
            }
            StringBuilder res = new StringBuilder();
            if (internal) {
                res.append("Internal SD card");
            } else if (displayNumber > 1) {
                res.append("SD card " + displayNumber);
            } else {
                res.append("SD card");
            }
            if (readonly) {
                res.append(" (Read only)");
            }
            mDisplayName = res.toString();
        }

        /**
         * getmDisplayName
         *
         * @return getmDisplayName
         */
        public String getmDisplayName() {
            return mDisplayName;
        }

        /**
         * setmDisplayName
         *
         * @param val String
         */
        public void setmDisplayName(String val) {
            mDisplayName = val;
        }
    }

    /**
     * returns all storage paths, writable or not
     *
     * @return List StorageInfo
     */
    public static List<StorageInfo> getStorageList() {

        List<StorageInfo> list = new ArrayList<StorageInfo>();
        String defPath = "";
        boolean defPathInternal = false;
        String defPathState = "";
        boolean defPathReadonly = true;
        boolean defPathAvailable = false;
        try {
            if (Environment.getExternalStorageDirectory() != null) {
                defPath = Environment.getExternalStorageDirectory().getPath();
            }
        } catch (Throwable ex) {
            //trap for android studio layout editor and some for certain devices
            //see https://github.com/osmdroid/osmdroid/issues/508
            ex.printStackTrace();
        }
        try {
            defPathInternal = (Build.VERSION.SDK_INT >= 9)
                    && !Environment.isExternalStorageRemovable();
        } catch (Throwable ex) {
            //trap for android studio layout editor and some for certain devices
            //see https://github.com/osmdroid/osmdroid/issues/508
            ex.printStackTrace();
        }
        try {
            defPathState = Environment.getExternalStorageState();
        } catch (Throwable ex) {
            //trap for android studio layout editor and some for certain devices
            //see https://github.com/osmdroid/osmdroid/issues/508
            ex.printStackTrace();
        }
        try {
            defPathAvailable = defPathState.equals(Environment.MEDIA_MOUNTED)
                    || defPathState.equals(Environment.MEDIA_MOUNTED_READ_ONLY);
        } catch (Throwable ex) {
            //trap for android studio layout editor and some for certain devices
            //see https://github.com/osmdroid/osmdroid/issues/508
            ex.printStackTrace();
        }

        try {
            defPathReadonly = Environment.getExternalStorageState()
                    .equals(Environment.MEDIA_MOUNTED_READ_ONLY);
        } catch (Throwable ex) {
            //trap for android studio layout editor and some for certain devices
            //see https://github.com/osmdroid/osmdroid/issues/508
            ex.printStackTrace();
        }
        BufferedReader bufferedReader = null;
        try {
            HashSet<String> paths = new HashSet<String>();
            bufferedReader = new BufferedReader(new FileReader("/proc/mounts"));
            String line;
            int curDisplayNumber = 1;
            while ((line = bufferedReader.readLine()) != null) {
                if (line.contains("vfat") || line.contains("/mnt")) {
                    StringTokenizer tokens = new StringTokenizer(line, " ");
                    String unused = tokens.nextToken(); //device
                    String mountPoint = tokens.nextToken(); //mount point
                    if (paths.contains(mountPoint)) {
                        continue;
                    }
                    unused = tokens.nextToken(); //file system
                    List<String> flags = Arrays.asList(tokens.nextToken().split(",")); //flags
                    boolean readonly = flags.contains("ro");

                    if (mountPoint.equals(defPath)) {
                        paths.add(defPath);
                        list.add(0, new StorageInfo(defPath, defPathInternal, readonly, -1));
                    } else if (line.contains("/dev/block/vold")) {
                        if (!line.contains("/mnt/secure")
                                && !line.contains("/mnt/asec")
                                && !line.contains("/mnt/obb")
                                && !line.contains("/dev/mapper")
                                && !line.contains("tmpfs")) {
                            paths.add(mountPoint);
                            // if (isWritable(new File(mount_point+ File.separator)))
                            list.add(new StorageInfo(mountPoint,
                                    false, readonly, curDisplayNumber++));
                        }
                    }
                }
            }

            if (!paths.contains(defPath) && defPathAvailable && defPath.length() > 0) {
                list.add(0, new StorageInfo(defPath, defPathInternal, defPathReadonly, -1));
            }

        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
        Set<File> allStorageLocationsRevised = getAllStorageLocationsRevised();
        Iterator<File> iterator = allStorageLocationsRevised.iterator();
        while (iterator.hasNext()) {
            File next = iterator.next();
            boolean found = false;
            for (int i = 0; i < list.size(); i++) {
                if (list.get(i).path.equals(next.getAbsolutePath())) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                list.add(new StorageInfo(next.getAbsolutePath(), false, false, -1));
            }
        }

        return list;
    }

    /**
     * gets the best possible storage location by freespace
     *
     * @return File
     */
    public static File getStorage() {
        //Map<String, File> allStorageLocations = getAllStorageLocations();
        //if (allStorageLocations.isEmpty()){
        //use the current app's storage
        //  return Environment.getDataDirectory();
        //}

        StorageInfo ptr = null;
        List<StorageInfo> storageList = getStorageList();
        for (int i = 0; i < storageList.size(); i++) {
            StorageInfo storageInfo = storageList.get(i);
            if (!storageInfo.readonly && isWritable(new File(storageInfo.path))) {
                if (ptr != null) {
                    //compare free space
                    if (ptr.freeSpace < storageInfo.freeSpace) {
                        ptr = storageInfo;
                    }
                } else {
                    ptr = storageInfo;
                }
            }
        }
        if (ptr != null) {
            return new File(ptr.path);
        }
        //http://stackoverflow.com/questions/21230629/getfilesdir-vs-environment-getdatadirectory
        try {
            return Environment.getExternalStorageDirectory();
        } catch (Exception ex) {
            //trap for android studio layout editor and some for certain devices
            //see https://github.com/osmdroid/osmdroid/issues/508
            return null;
        }
    }

    /**
     * gets the best possible storage location by freespace
     *
     * @param ctx Context
     * @return File
     */
    public static File getStorage(final Context ctx) {
        //Map<String, File> allStorageLocations = getAllStorageLocations();
        //if (allStorageLocations.isEmpty()){
        //use the current app's storage
        //  return Environment.getDataDirectory();
        //}

        StorageInfo ptr = null;
        List<StorageInfo> storageList = getStorageList();
        for (int i = 0; i < storageList.size(); i++) {
            StorageInfo storageInfo = storageList.get(i);
            if (!storageInfo.readonly && isWritable(new File(storageInfo.path))) {
                if (ptr != null) {
                    //compare free space
                    if (ptr.freeSpace < storageInfo.freeSpace) {
                        ptr = storageInfo;
                    }
                } else {
                    ptr = storageInfo;
                }
            }
        }
        if (ptr != null) {
            return new File(ptr.path);
        }
        //http://stackoverflow.com/questions/21230629/getfilesdir-vs-environment-getdatadirectory
        return new File(ctx.getDatabasePath("temp.sqlite")
                .getAbsolutePath().replace("temp.sqlite", ""));
    }

    /**
     * @return True if the external storage is available. False otherwise.
     */
    public static boolean isAvailable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state)
                || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
            return true;
        }
        return false;
    }

    public static String getSdCardPath() {
        return Environment.getExternalStorageDirectory().getPath() + "/";
    }

    /**
     * @return True if the external storage is writable. False otherwise.
     */
    public static boolean isWritable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state)) {
            return true;
        }
        return false;

    }

    /**
     * isWritable
     *
     * @param path file
     * @return boolean
     */
    public static boolean isWritable(File path) {
        //if (path.exists())
        {
            //if (path.canWrite())
            {
                //try to create a new file, save it, then delete it
                try {

                    File tmp = new File(path.getAbsolutePath() + File.separator + "osm.tmp");
                    FileOutputStream fos = new FileOutputStream(tmp);
                    fos.write("hi".getBytes());
                    fos.close();
                    tmp.delete();
                    return true;
                } catch (Throwable ex) {
                    return false;
                }
            }
        }
        //return false;

    }

    /**
     * @return A map of all storage locations available
     */
    public static Map<String, File> getAllStorageLocations() {
        Map<String, File> map = new HashMap<String, File>(10);

        List<String> mounts = new ArrayList<String>(10);
        List<String> vold = new ArrayList<String>(10);
        mounts.add("/mnt/sdcard");
        vold.add("/mnt/sdcard");

        try {
            File mountFile = new File("/proc/mounts");
            if (mountFile.exists()) {
                Scanner scanner = new Scanner(mountFile);
                while (scanner.hasNext()) {
                    String line = scanner.nextLine();
                    if (line.startsWith("/dev/block/vold/")) {
                        String[] lineElements = line.split(" ");
                        String element = lineElements[1];

                        // don't add the default mount path
                        // it's already in the list.
                        if (!element.equals("/mnt/sdcard")) {
                            mounts.add(element);
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            File voldFile = new File("/system/etc/vold.fstab");
            if (voldFile.exists()) {
                Scanner scanner = new Scanner(voldFile);
                while (scanner.hasNext()) {
                    String line = scanner.nextLine();
                    if (line.startsWith("dev_mount")) {
                        String[] lineElements = line.split(" ");
                        String element = lineElements[2];

                        if (element.contains(":")) {
                            element = element.substring(0, element.indexOf(":"));
                        }
                        if (!element.equals("/mnt/sdcard")) {
                            vold.add(element);
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        for (int i = 0; i < mounts.size(); i++) {
            String mount = mounts.get(i);
            if (!vold.contains(mount)) {
                mounts.remove(i--);
            }
        }
        vold.clear();

        List<String> mountHash = new ArrayList<String>(10);

        for (String mount : mounts) {
            File root = new File(mount);
            if (root.exists() && root.isDirectory() && root.canWrite()) {
                File[] list = root.listFiles();
                String hash = "[";
                if (list != null) {
                    for (File f : list) {
                        hash += f.getName().hashCode() + ":" + f.length() + ", ";
                    }
                }
                hash += "]";
                if (!mountHash.contains(hash)) {
                    String key = SD_CARD + "_" + map.size();
                    if (map.size() == 0) {
                        key = SD_CARD;
                    } else if (map.size() == 1) {
                        key = EXTERNAL_SD_CARD;
                    }
                    mountHash.add(hash);
                    map.put(key, root);
                }
            }
        }

        mounts.clear();

        if (map.isEmpty()) {
            map.put(SD_CARD, Environment.getExternalStorageDirectory());
        }

        //ok now that we've done the dirty linux work, let's pull in the android bits
        if (!map.containsValue(Environment.getExternalStorageDirectory())) {
            map.put(SD_CARD, Environment.getExternalStorageDirectory());
        }

        String primarySd = System.getenv("EXTERNAL_STORAGE");
        if (primarySd != null) {
            File file = new File(primarySd);
            if (file.exists() && !map.containsValue(file)) {
                map.put(SD_CARD, file);
            }
        }

        String secondarySd = System.getenv("SECONDARY_STORAGE");
        if (secondarySd != null) {
            String[] split = secondarySd.split(File.pathSeparator);
            for (int i = 0; i < split.length; i++) {
                File file = new File(split[i]);
                if (file.exists() && !map.containsValue(file)) {
                    map.put(SD_CARD, file);
                }
            }
        }

        return map;
    }

    /**
     * @return A map of all storage locations available
     */
    private static Set<File> getAllStorageLocationsRevised() {

        Set<File> map = new HashSet<>();
        String primarySd = System.getenv("EXTERNAL_STORAGE");
        if (primarySd != null) {
            File file = new File(primarySd + File.separator);
            if (isWritable(file)) {
                map.add(file);
            }
        }

        String secondarySd = System.getenv("SECONDARY_STORAGE");
        if (secondarySd != null) {
            String[] split = secondarySd.split(File.pathSeparator);
            for (int i = 0; i < split.length; i++) {
                File file = new File(split[i] + File.separator);
                if (isWritable(file)) {
                    map.add(file);
                }
            }
        }

        if (Environment.getExternalStorageDirectory() != null) {
            File file = Environment.getExternalStorageDirectory();
            if (isWritable(file)) {
                map.add(file);
            }
        }

        List<String> mounts = new ArrayList<String>(10);
        List<String> vold = new ArrayList<String>(10);
        mounts.add("/mnt/sdcard");
        vold.add("/mnt/sdcard");

        try {
            File mountFile = new File("/proc/mounts");
            if (mountFile.exists()) {
                Scanner scanner = new Scanner(mountFile);
                while (scanner.hasNext()) {
                    String line = scanner.nextLine();
                    if (line.startsWith("/dev/block/vold/")) {
                        String[] lineElements = line.split(" ");
                        String element = lineElements[1];

                        // don't add the default mount path
                        // it's already in the list.
                        if (!element.equals("/mnt/sdcard")) {
                            mounts.add(element);
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            File voldFile = new File("/system/etc/vold.fstab");
            if (voldFile.exists()) {
                Scanner scanner = new Scanner(voldFile);
                while (scanner.hasNext()) {
                    String line = scanner.nextLine();
                    if (line.startsWith("dev_mount")) {
                        String[] lineElements = line.split(" ");
                        String element = lineElements[2];

                        if (element.contains(":")) {
                            element = element.substring(0, element.indexOf(":"));
                        }
                        if (!element.equals("/mnt/sdcard")) {
                            vold.add(element);
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        for (int i = 0; i < mounts.size(); i++) {
            String mount = mounts.get(i);
            if (!vold.contains(mount)) {
                mounts.remove(i--);
            }
        }
        vold.clear();

        List<String> mountHash = new ArrayList<String>(10);

        for (String mount : mounts) {
            File root = new File(mount);
            if (root.exists() && root.isDirectory() && root.canWrite()) {
                File[] list = root.listFiles();
                String hash = "[";
                if (list != null) {
                    for (File f : list) {
                        hash += f.getName().hashCode() + ":" + f.length() + ", ";
                    }
                }
                hash += "]";
                if (!mountHash.contains(hash)) {
                    String key = SD_CARD + "_" + map.size();
                    if (map.size() == 0) {
                        key = SD_CARD;
                    } else if (map.size() == 1) {
                        key = EXTERNAL_SD_CARD;
                    }
                    mountHash.add(hash);
                    if (isWritable(root)) {
                        map.add(root);
                    }
                }
            }
        }
        mounts.clear();
        return map;
    }

    /**
     * 获取应用专属缓存目录
     * android 4.4及以上系统不需要申请SD卡读写权限
     * 因此也不用考虑6.0系统动态申请SD卡读写权限问题，切随应用被卸载后自动清空 不会污染用户存储空间
     *
     * @param context 上下文
     * @param type    文件夹类型 可以为空，为空则返回API得到的一级目录
     * @return 缓存文件夹 如果没有SD卡或SD卡有问题则返回内存缓存目录，否则优先返回SD卡缓存目录
     */
    public static File getCacheDirectory(Context context, String type) {
        File appCacheDir = getExternalCacheDirectory(context, type);
        if (appCacheDir == null) {
            appCacheDir = getInternalCacheDirectory(context, type);
        }

        if (appCacheDir == null) {
            Log.e("getCacheDirectory", "getCacheDirectory fail "
                    + ",the reason is mobile phone unknown exception !");
        } else {
            if (!appCacheDir.exists() && !appCacheDir.mkdirs()) {
                Log.e("getCacheDirectory", "getCacheDirectory fail "
                        + ",the reason is make directory fail !");
            }
        }
        return appCacheDir;
    }

    /**
     * 获取SD卡缓存目录
     *
     * @param context 上下文
     * @param type    文件夹类型 如果为空则返回 /storage/emulated/0/Android/data/
     *                app_package_name/cache
     *                否则返回对应类型的文件夹如Environment.DIRECTORY_PICTURES 对应的文件夹为
     *                .../data/app_package_name/files/Pictures
     *                {@link Environment#DIRECTORY_MUSIC},
     *                {@link Environment#DIRECTORY_PODCASTS},
     *                {@link Environment#DIRECTORY_RINGTONES},
     *                {@link Environment#DIRECTORY_ALARMS},
     *                {@link Environment#DIRECTORY_NOTIFICATIONS},
     *                {@link Environment#DIRECTORY_PICTURES}, or
     *                {@link Environment#DIRECTORY_MOVIES}.or 自定义文件夹名称
     * @return 缓存目录文件夹 或 null（无SD卡或SD卡挂载失败）
     */
    public static File getExternalCacheDirectory(Context context, String type) {
        File appCacheDir = null;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
            if (TextUtils.isEmpty(type)) {
                appCacheDir = context.getExternalCacheDir();
            } else {
                appCacheDir = context.getExternalFilesDir(type);
            }

            if (appCacheDir == null) {
                // 有些手机需要通过自定义目录
                appCacheDir = new File(Environment.getExternalStorageDirectory(),
                        "Android/data/" + context.getPackageName() + "/cache/" + type);
            }

            if (appCacheDir == null) {
                Log.e("getExternalDirectory", "getExternalDirectory fail "
                        + ",the reason is sdCard unknown exception !");
            } else {
                if (!appCacheDir.exists() && !appCacheDir.mkdirs()) {
                    Log.e("getExternalDirectory",
                            "getExternalDirectory fail ,the reason is "
                                    + "make directory fail !");
                }
            }
        } else {
            Log.e("getExternalDirectory",
                    "getExternalDirectory fail ,the reason is sdCard "
                            + "nonexistence or sdCard mount fail !");
        }
        return appCacheDir;
    }

    /**
     * 获取内存缓存目录
     *
     * @param type    子目录，可以为空，为空直接返回一级目录
     * @param context context
     * @return 缓存目录文件夹 或 null（创建目录文件失败）
     * 注：该方法获取的目录是能供当前应用自己使用，外部应用没有读写权限，如 系统相机应用
     */
    public static File getInternalCacheDirectory(Context context, String type) {
        File appCacheDir = null;
        if (TextUtils.isEmpty(type)) {
            appCacheDir = context.getCacheDir();
            // /data/data/app_package_name/cache
        } else {
            appCacheDir = new File(context.getFilesDir(), type);
            // /data/data/app_package_name/files/type
        }

        if (!appCacheDir.exists() && !appCacheDir.mkdirs()) {
            Log.e("getInternalDirectory",
                    "getInternalDirectory fail ,the reason is make directory fail !");
        }
        return appCacheDir;
    }
}
